"""
Diese Klasse ist zustaendig fuer fast alle Funktionen im Spiel.
Hier wird ein Level aufgebaut, die Kollision mit Objekten und Waenden abgefragt und die Map bei Bewegungen neu zentriert.
Desweiteren Speicher- und Ladevorgaenge und Anzeigen von HUDs (Inventar, Optionen, Interaktionen).    
"""

from operator import add
import os
import shelve

from pytmx import tmxloader
import pygame
import pyscroll

import Core  # @UnresolvedImport
import HUD
import Resources  # @UnresolvedImport

class Level(pygame.Surface):
    def __init__(self, res, log, rs, player=None):
        pygame.Surface.__init__(self, res)
        
        self.tplResolution = res
        self.Log = log
        self.RenderScreen = rs
        
        self.bgColor = 0
        
        self.bLoadLevel = True
        self.strLevelName = 0
        
        # Map-Variablen
        self.mapTMX = 0
        self.mapData = 0
        
        self.iMidMapX = 23 * 16
        self.iMidMapY = 17 * 16
        
        self.mapXoffset = 24
        self.mapYoffset = 120
        
        self.mapLayer = 0
        self.mapSpriteGroup = 0
        
        self.tplTilePos = (0, 0)
        self.tplDestPos = (0, 0)
        self.tplFirstTile = (0, 0)
        
        # Bewegungs-Variablen
        self.bMovementFinished = True
        self.bFirstMove = False
        self.bLevelChange = False
        
        self.bStopMovement = False
        
        self.evtFinishMovement = None
        
        self.iFoot = 1
        
        self.iTSLF = 0
        self.iAccel = 100
        
        # Hud-Aufbau
        self.HUD = None
        self.iHUDx = 0
        self.iHUDy = 0
        
        # Player
        self.Player = player
        
        # NPC
        self.listNPC = []
        self.elemNPC = None
        
        # Inventar
        self.Inventory = HUD.Inventory((self.tplResolution[0] * 0.8, self.tplResolution[1] * 0.8), self.Log)  # @UndefinedVariable
        self.bShowInventory = False
    
        # Item-List
        self.arItems = []
            
    def update(self, dt): 
        """
        Update wird bei jedem Durchlauf der GameLoop durchlaufen und fuehrt alle noetigen neuen Berechnungen aus
        """ 
        self.iTSLF = dt
        bRunning = True
            
        if self.bMovementFinished != True:
            self.finishMovement()
        else:
            if self.bStopMovement:
                self.Player.setPlayerDirection(0)
                self.bStopMovement = False             
            bRunning = self.handleInput()  
            if self.HUD:
                self.HUD.update()
                     
        self.checkCollisionWithItem()    
        self.mapLayer.update(dt)
        
        
        return bRunning
    
    def render(self):
        """
        Render ist dafuer zustaendig den neuesten aktuellsten Stand des Spiels anzuzeigen.
        Je nachdem, ob ein HUD aktiv ist, wird die Map nicht jedes mal erneut gezeichnet, sondern im Hintergrund belassen.
        """
        if self.bShowInventory:
            self.Inventory.render()
            self.blit(self.Inventory.surfInventory, (self.tplResolution[0] * 0.1, self.tplResolution[1] * 0.1))
        else:                    
            if self.HUD == None:
                self.mapLayer.draw(self, self.get_rect())
                
                if self.arItems > 0:
                    for elem in self.arItems:
                        elem.render()
                        self.blit(elem.surfItem, elem.tplPos)
            else:
                self.Player.setPlayerDirection(0)
                self.HUD.render(self.iTSLF)  
                self.blit(self.HUD.surfHud, (self.iHUDx, self.iHUDy))
                
                    
            self.Player.render()
            self.blit(self.Player.surfPlayer, (self.iMidMapX, self.iMidMapY))   
     
    def checkCollisionWithItem(self):
        """
        Falls der Spieler mit einem Item kollidiert, wird dieses ins Inventar verschoben und von der Map geloescht.
        """
        if len(self.arItems) > 0:
            for elem in self.arItems:
                if abs(elem.tplPos[0] - self.iMidMapX) < 1.0 and abs(elem.tplPos[1] - self.iMidMapY) < 1.0:
                    strName = "inv_" + str(self.Inventory.iInvIndex)  # @UnusedVariable
                    self.arItems.remove(elem)
                    self.Inventory.iInvIndex += 1
                    
                    elem.imgItem = pygame.transform.scale(elem.imgItem, (32, 32))
                    elem.surfItem = pygame.Surface((32, 32)).convert()
                    elem.surfItem.set_colorkey(Resources.Colors.BLACK)
                    elem.surfItem.blit(elem.imgItem, (0, 0))
                     
                    self.Inventory.addItemToInventory(elem)
                        
    def checkCollisionToStopMotion(self):
        """
        Hier wird bei einer initiierten Bewegung geschaut, ob das Ziel-Tile begehbar ist und ob es sich um eine Kollision handelt. 
        Die verschiedenen Kollisionstypen sind "Trigger" fuer Tueren, "Kollision" fuer nicht begehbare Tiles und "NPC" fuer nicht begehbare Tiles, die aber eine Interaktion ermoeglichen.
        Wenn das Ziel nicht begehbar ist liefert diese Methode "True", andernfalls "False".
        """
        for enem in self.mapTMX.getObjects():
            if (self.tplDestPos[0] >= enem.x + self.mapXoffset and self.tplDestPos[0] <= enem.x + self.mapXoffset + enem.width)  \
                and                                                                                                              \
               (self.tplDestPos[1] >= enem.y + self.mapYoffset and self.tplDestPos[1] <= enem.y + self.mapYoffset + enem.height):
                if enem.type == "Trigger":
                    self.Log.printToLog("Level: Kollision mit Trigger %s." % enem.name)
                    if enem.name[0:5] == "level":
                        self.Player.setPlayerDirection(0)
                        newLevel = Level(self.tplResolution, self.Log, self.RenderScreen, self.Player)
                        newLevel.loadLevel(enem.name)
                        newLevel.Inventory = self.Inventory
                        newLevel.bMovementFinished = True
                        self.RenderScreen.currentScene = newLevel
                        return True
                elif enem.type == "Kollision":
                    self.Log.printToLog("Level: Kollision mit Hindernis %s." % enem.name)
                    self.Player.setPlayerDirection(0)
                    return True
                elif enem.type == "NPC":
                    self.Log.printToLog("Level: Kollision mit Schild %s." % enem.name)
                    self.Player.setPlayerDirection(0)
                    return True          
        return False
            
    def loadLevel(self, levelname, bLoadingLevel=False): 
        """
        Diese Methode baut das Level auf. Als Parameter wird ein Levelname, wie "level1","level2" erwartet.
        Der Name muss dem Namen der .tmx-File entsprechen, damit das File geladen werden kann.
        """
        self.Log.printToLog("Level: Level %s wird geladen..." % (levelname))
        pthLevel = os.path.join("..", "Resources", "Levels", levelname + ".tmx")
        self.strLevelName = levelname
        
        if not os.path.exists(pthLevel):
            self.Log.printToLog("Level: Leveldatei wurde nicht gefunden: %s" % (pthLevel))
        else:
            self.Log.printToLog("Level: Leveldatei gefunden")
            self.mapTMX = tmxloader.load_pygame(pthLevel, pixelalpha=True)
            
            self.mapData = pyscroll.TiledMapData(self.mapTMX)
            self.mapLayer = pyscroll.BufferedRenderer(self.mapData, (49 * self.mapTMX.tileheight, 49 * self.mapTMX.tilewidth))
            
            for elem in self.mapTMX.getObjects():
                if elem.type == "Spawn":
                    self.tplTilePos = (elem.x + self.mapXoffset, elem.y + self.mapYoffset)
                    self.tplFirstTile = self.iMidMapX - elem.x, self.iMidMapY - elem.y
                elem.visible = False
            
            self.mapLayer.center(self.tplTilePos)
                        
            if self.Player == None:    
                self.Player = Core.Player(self.Log)
                
            if bLoadingLevel:
                self.Player.iCurrentPosition = 0
                self.Player.imgPlayer = self.Player.playerTMX.getTileImage(self.Player.iCurrentPosition, 0, 0)
            
            pygame.key.set_repeat(50, 1)
            self.bLoadLevel = False
         
        # Test-Items  
        self.arItems.append(Core.Item(self.Log, (15, 10), self.tplFirstTile, 1))  # @UndefinedVariable
        self.arItems.append(Core.Item(self.Log, (20, 10), self.tplFirstTile, 1))  # @UndefinedVariable
        self.arItems.append(Core.Item(self.Log, (15, 15), self.tplFirstTile, 2))  # @UndefinedVariable
         
    def movementInitiated(self, event):
        """
        Bei Betaetigung von WASD wird diese Funktion aufgerufen und die entsprechende Richtungsbewegung initialisiert.
        Bevor jedoch die eigentliche Bewegung erfolgt, wird auf Kollision ueberprieft.
        Damit ein fluessiger Uebergang vom Start zum Ziel entsteht, werden Zwischenschritte interpoliert.
        """ 
        
        if not self.Player.playerInCorrectPosition(event.key): 
            self.Player.correctPosition(event)

        self.evtFinishMovement = event
    
        dirVect, dest = self.Player.movePlayer(event.key, self.iTSLF, self.iAccel)
                
        # Am Anfang muss die Ziel-Tile gesetzt werden
        if self.bFirstMove:
            self.bFirstMove = False
            self.tplDestPos = tuple(map(add, self.tplTilePos, dest))
            
        if self.checkCollisionToStopMotion():
            self.bMovementFinished = True
            
        else:
            # Nach jedem Schritt wird die Position neu gesetzt, damit korrekt gezeichnet wird
            self.tplTilePos = tuple(map(add, self.tplTilePos, dirVect))
            
            for elem in self.arItems:
                elem.tplPos = (elem.tplPos[0] - dirVect[0], elem.tplPos[1] - dirVect[1])
                
            if self.tplTilePos == self.tplDestPos:
                self.bMovementFinished = True
                pygame.event.clear()
         
            self.mapLayer.scroll(dirVect)
    
    def finishMovement(self):
        """
        Diese Funktion sorgt fuer die Interpolation der Bewegung, falls der Spieler noch nicht auf dem Ziel steht.
        """   
        self.Player.setPlayerDirection(self.iFoot)
        self.handleInputWhileMovement()
        self.movementInitiated(self.evtFinishMovement)
    
    def executeInteraction(self, enem=None):
        """
        Bei Betaetigen der Leertaste wird diese Funktion aufgerufen, die je nach Zustand des Spiels verschieden reagiert.
        Ist ein HUD geoeffnet, wird der angezeigte Text geaendert, bis kein Text mehr vorhanden ist.
        Befindet sich der Spieler auf der Map wird geschaut, ob das Ziel-Tile eine Interaktion erlaubt und ggf. diese ausgefuehrt.
        """
        bNoElem = True

        if enem == None:
            for elem in self.mapTMX.getObjects():
                if elem.type == "NPC":
                    if self.tplDestPos == (elem.x + self.mapXoffset, elem.y + self.mapYoffset):
                        bNoElem = False
                        enem = elem
        else:
            bNoElem = False
                
        if bNoElem:
            self.Log.printToLog("Level: Hier ist keine Interaktion moeglich.")
        else:
            self.HUD = HUD.Text(self.Log, (self.tplResolution[0], self.tplResolution[1] * 0.21), Resources.Messages.NPC_Text[self.strLevelName][enem.name])  
            self.iHUDx = 0
            self.iHUDy = self.tplResolution[1] * 0.8
            pygame.key.set_repeat(0, 0)
                                   
    def handleInput(self):
        """
        Hier wird der Input ueber die Tastatur verarbeitet
        """ 
        if self.bMovementFinished == True:
            for event in pygame.event.get():
                if self.HUD:
                    # Wenn ein HUD geoeffnet wird, ist dieses zustaendig fuer den Input
                    iStatusCode = self.HUD.handleInput(event)
                    bStatus = False
                    if iStatusCode >= 0:
                        bStatus = True
                        
                    if iStatusCode == 1:
                        self.saveGame()     
                        
                    if iStatusCode == 2:
                        self.loadGame()               
                    
                    if self.HUD.bCloseHud:
                        self.HUD = None
                        self.elemNPC = None
                        pygame.key.set_repeat(50, 1)
                        
                    return bStatus           
                else:
                    if event.type == pygame.KEYDOWN:
                        if not self.bShowInventory:
                            if event.key == pygame.K_d or event.key == pygame.K_w or event.key == pygame.K_a or event.key == pygame.K_s:
                                self.bMovementFinished = False
                                self.bFirstMove = True 
                                self.selectFoot()
                                self.movementInitiated(event)
                            elif event.key == pygame.K_SPACE:
                                self.Player.setPlayerDirection(0)
                                self.executeInteraction()
                            elif event.key == pygame.K_ESCAPE:
                                self.openOptions()
                        if event.key == pygame.K_i:
                            if self.bShowInventory:
                                self.Log.printToLog("Level: Inventar geschlossen.")
                                self.bShowInventory = False
                                pygame.key.set_repeat(50, 1)
                            else:
                                self.Log.printToLog("Level: Inventar geoeffnet.")
                                self.bShowInventory = True
                                pygame.key.set_repeat(0, 0)
                    elif event.type == pygame.QUIT:
                        return False
        return True 
    
    def openOptions(self):
        """
        Bei ESC oeffnet sich das Options-Fenster.
        """
        self.HUD = HUD.Options(self.Log, (self.tplResolution[0] * 0.30, self.tplResolution[1]))
        self.iHUDx = self.tplResolution[0] * 0.70
        self.iHUDy = 0
        pygame.key.set_repeat(0, 0)
        
    def handleInputWhileMovement(self):
        """
        Diese Funktion stellt sicher, dass die Figur wieder auf einer "Stehend"-Tile endet, sodass es nicht aussieht, als ob die Figur sich bewegt, obwohl sie steht.
        Hierzu wird eine Bool-Variable auf True gesetzt, sobald eine Bewegungstaste losgelassen wurde.
        Nach Aufruf dieser Funktion wird ueberprueft, ob die genannte Bool-Variable auf True steht, um die Tile zu aendern.
        """
        for event in pygame.event.get():
            if event.type == pygame.KEYUP:
                self.bStopMovement = True
                
    def selectFoot(self):
        """
        Hier wird festgestellt, welcher Fuss als naechstes fuer die Bewegung verwendet wird.
        """
        if self.Player.bFoot == False:
            self.iFoot = 1
            self.Player.bFoot = True
        else:
            self.iFoot = 2
            self.Player.bFoot = False
                
    def saveGame(self):
        """
        Speichert einen Spielstand
        """
        shelfFile = shelve.open(os.path.join("..", "Resources", "Assets", "save.dat"))
        shelfFile['level'] = self.strLevelName
        shelfFile['pos'] = self.tplTilePos
        shelfFile['firsttile'] = self.tplFirstTile
        index = 0
        for elem in self.Inventory.dictInventory:
            shelfFile["invpl_" + str(index)] = elem.iID
            shelfFile["invpl_" + str(index) + "_amount"] = self.Inventory.dictInventory[elem]
            index += 1
        shelfFile['inv_size'] = len(self.Inventory.dictInventory)

        shelfFile.close()
        self.Log.printToLog("Level: Spielstand erfolgreich gespeichert")
        
    def loadGame(self):
        """
        Laed ein gespeichertes Spiel
        """
        shelfFile = shelve.open(os.path.join("..", "Resources", "Assets", "save.dat"))

        loadLevel = Level(self.tplResolution, self.Log, self.RenderScreen, self.Player)
        loadLevel.loadLevel(shelfFile['level'], True)
        loadLevel.Inventory = HUD.Inventory((self.tplResolution[0] * 0.8, self.tplResolution[1] * 0.8), self.Log)
        loadLevel.tplTilePos = shelfFile['pos']
        loadLevel.tplFirstTile = shelfFile['firsttile']
        loadLevel.arItems = []
        iInvSize = shelfFile['inv_size']
        
        for i in range(0, iInvSize):
            amount = shelfFile["invpl_" + str(i) + "_amount"]
            for i in range (0, amount):
                newInventoryItem = Core.Item(self.Log, (0, 0), self.tplFirstTile, shelfFile["invpl_" + str(i)])
                newInventoryItem.imgItem = pygame.transform.scale(newInventoryItem.imgItem, (32, 32))
                newInventoryItem.surfItem = pygame.Surface((32, 32)).convert()
                newInventoryItem.surfItem.set_colorkey(Resources.Colors.BLACK)
                newInventoryItem.surfItem.blit(newInventoryItem.imgItem, (0, 0))
                
                loadLevel.Inventory.addItemToInventory(newInventoryItem)

        loadLevel.mapLayer.center(loadLevel.tplTilePos)
        
        self.RenderScreen.currentScene = loadLevel
        
        shelfFile.close()
        self.Log.printToLog("Level: Spielstand erfolgreich geladen")
